
這章不會教大家怎麼安裝 Flutter,而是會以一個比較宏觀的角度告訴大家一個 Flutter App 的架構。
本次教學使用 Flutter 3.24 進行。本章節的範例程式碼:https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/hello
但建議讀者可以自行在本地以指令建立。
首先我們可以在 Flutter 官網中安裝 Flutter https://flutter.dev.org.tw/get-started/install
因為每個人電腦環境狀況不一樣,因此這裡不提供這方面的教學。另外,我們也可以使用 Google Project IDX 來新增一個 Flutter 專案
在使用 flutter 指令前,我們可以先使用 flutter doctor 來測試 flutter 是否安裝成功
> flutter doctor

像我目前的裝置,因為沒有要開發 windows app 所以可以不安裝 Visual Studio
我們可以直接利用命令列新增一個簡單的 flutter 專案,直接命名成 hello
> flutter create hello
以下是整個專案的結構,我們主要的程式在 lib 資料夾中
.
└── hello/
    ├── .dart_tool/  # Dart 編譯器和工具所使用的內部資料夾,存放暫存資料和依賴管理
    ├── .idea/       # IDE 設定檔(通常為 JetBrains 系列,如 IntelliJ 或 Android Studio)
    ├── android/     # Android 平台專用的原生程式碼與配置,當部署到 Android 時會用到
    ├── ios/         # iOS 平台專用的原生程式碼與配置,當部署到 iOS 時會用到
    ├── lib/         # 放置主要的 Dart 程式碼,包括主應用程式邏輯
    ├── linux/       # Linux 平台專用的原生程式碼與配置,當部署到 Linux 時會用到
    ├── macos/       # macOS 平台專用的原生程式碼與配置,當部署到 macOS 時會用到
    ├── test/        # 測試程式碼放置處,用來進行單元測試或其他自動化測試
    ├── web/         # Web 平台專用的原生程式碼與配置,當部署到 Web 時會用到
    ├── windows/     # Windows 平台專用的原生程式碼與配置,當部署到 Windows 時會用到
    ├── .gitignore   # 定義哪些檔案或資料夾不會被 Git 版本控制系統追蹤
    ├── .metadata    # Flutter 專案的元數據檔案,包含一些版本與配置資訊
    ├── analysis_options.yaml  
                     # Dart 分析器的配置檔案,用來控制靜態分析的行為
    ├── hello.iml     # IntelliJ IDEA 專案檔,儲存 IDE 專案設定
    ├── pubspec.lock # 鎖定依賴版本的檔案,確保每次構建使用的依賴版本一致
    ├── pubspec.yaml # Flutter 專案的配置檔案,定義專案的依賴、版本等資訊
    └── README.md    # 專案的說明文件,通常包含簡單介紹、如何使用、安裝等資訊
初始化後,我們可以馬上執行專案
> flutter run
此時會跳出幾個選項,我們可以直接使用 Chrome 或 Edge 來查看專案

選擇 2 後,Flutter 會自動開啟一個新的 Chrome 瀏覽器運行 Flutter 專案

為了更改 APP 的內容,我們可以更改專案中的 lib/main.dart 檔
import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // ...
  }
}
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  // ...
  @override
  Widget build(BuildContext context) {
    // ...
  }
}
首先,Flutter 專案中一樣有個 main 函式,main 函式裡會調用 runApp(),runApp() 必需要傳入一個 Widget 物件
void runApp(Widget app)
在 Flutter 中有兩大 Widget:StatelessWidget 以及 StatefulWidget
StatelessWidget 是無狀態的 widget,代表一個不會改變的 UI 元素。當 widget 被 build 之後,它的狀態不會發生變化。因此,如果 UI 的外觀或行為只需根據輸入的參數進行渲染,且不需要動態更新,StatelessWidget 是一個好的選擇。
使用場景:
StatefulWidget 是有狀態的 widget,它的 UI 可以在 widget 的生命週期中發生變化。它由兩部分組成:
StatefulWidget:定義 widget 是什麼,但不保存狀態。State:保存與 widget 相關的狀態,並在**「狀態變更」**時重新渲染 UI。當 widget 的狀態需要隨時間改變,例如按鈕被點擊後會有不同的反應,或是從網路載入數據時使用進度條,StatefulWidget 就派上用場。
使用場景:
我們可以將 Widget 想像成一棵樹 (資料結構的樹),runApp 會所放的就是 MyApp 這個根節點
runApp → MyApp → MyHomePage
在使用 StatelessWidget 時,我們必需先建立建構函式,因為建構函式建構 MyApp 後不會再更動所以我們會加上 const,並且當 flutter 在建構這些 Widget 時,都會給予 key 值,也因此我們可以把得到的 key 值傳遞給父母類 StatelessWidget。
接著我們需要 @override 一個函式 build(), build 函式會在該 widget 被加入樹時,或者一些相依性更動時被呼叫。現在,我們其實只需要關注 build 函式內的 return 就好,return 需要回傳一個 Widget,在 flutter 中,我們最外層的 Widget 會使用 MaterialApp。Material 是 Android 的設計風格。除了以 MaterialApp 作為起點,我們其實也可以使用 ios 風格的 CupertinoApp。
class MyApp extends StatelessWidget {
  // 建構函式,
  const MyApp({super.key});
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Notice that the counter didn't reset back to zero; the application
        // state is not lost during the reload. To reset the state, use hot
        // restart instead.
        //
        // This works for code too, not just values: Most code changes can be
        // tested with just a hot reload.
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
MateralApp 建構時有很多地方可供調整,如果要繼續往下搭建我們的畫面,就必需把我們的主畫面 Widget 放入 home 之中。MyHomePage 是一個 StatefulWidget。使用 StatefulWidget 時,也要先建立建構函式,除了提供 key 值之外,我們也可以另外設定自己需要的參數,比如 title (StatelessWidget 也可以只是這裡示範的是 StatefulWidget)。接著我們要 @override createState() 這個方法,createState 的功能是建構並返回一個 State 物件,這個 State 物件會保存 widget 的狀態並控制其重建。而這個 State 物件就是我們的關鍵。
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.
  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
我們可以看到我們自定的 State 物件相當複雜,其中最重要的就是 build() 函式,負責建構畫面。我們這裡會使用 Scaffold Widget 來建構畫面,主要包含三個部分:appBar, body 以及 floatingActionButton
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

當我們點擊 floatingActionButton 時會呼叫 _incrementCounter 進而影響 body 畫面中的數字,我們可以看到 _incrementCounter 會呼叫 setState 函式,其實 setState 的作用就是告訴 flutter 目前我們的 StatefulWidget 中的狀態已經被改變了,需要對 StatefulWidget 重新渲染。
void _incrementCounter() {
  setState(() {
    _counter++;
  });
}
setState() 中的 callback function 會先被執行,執行完畢後會將 StatefulWidget 標記為需要進行畫面更新。因此 setState() 中的 callback function 不可以是 async function
void setState(VoidCallback fn){
  assert(...);  // 檢查一些生命週期
  final Object? result = fn() as dynamic;  // 呼叫 fn
  assert(...);	// 檢查 fn 是否為 async function
  _element!.markNeedsBuild();  // 呼叫 markNeedsBuild()
  // markNeedsBuild() 會將元件設為 dirty 並將其加入全局 list 中
  // 告訴下一個 frame 需要將此元件重新建構
}
setState()的作用範圍是 statefulWidget 下的 widget (subtree),也因此如果我們需要變更在我們之上的 widget 必需要用其他的方法!
我們可以更改 _incrementCounter 使其每次都加 2
void _incrementCounter() {
  setState(() {
    _counter += 2;
  });
}
當更改後可以在 terminal 上按下 r ,這個會使 Flutter 熱重啟整個程式!按 q 則會關掉程式
後記:明天應該會講 Inheritedwidget